编译期优化-实战

编译期优化-实战

第四节 实战:插入式注解处理器

  JDK编译优化部分在本书中并没有设置独立的实战章节,因为我们开发程序,考虑的主要是程序会如何运行,很少会有针对程序编译的需求。也因为这个原因,在JDK的编译子系统里面,提供给用户直接控制的功能相对较少,除了第11章会介绍的虚拟机JIT编译的几个相关参数以外,我们就只有使用JSR-296中定义的插入式注解处理器API来对JDK编译子系统的行为产生一些影响。

  但是作者并不认为相对于前两部分介绍的内存管理子系统和字节码执行子系统,JDK的编译子系统就不那么重要。一套编程语言中编译子系统的优劣,很大程度上决定了程序运行性能的好坏和编码效率的高低,尤其在Java语言中,运行期即时编译与虚拟机执行子系统非常紧密地互相依赖、配合运作(第11章将主要讲解这方面的内容)。了解JDK如何编译和优化代码,有助于我们写出适合JDK自优化的程序。下面我们回到本章的实战中,看看插入式注解处理器API能实现什么功能。

4.1 实战目标

  通过阅读Javac编译器的源码,我们知道编译器在把Java程序源码编译为字节码的时候,会对Java程序源码做各方面的检查校验。这些校验主要以程序“写得对不对”为出发点,虽然也有各种WARNING的信息,但总体来讲还是较少去校验程序“写得好不好”。有鉴于此,业界出现了许多针对程序“写得好不好”的辅助校验工具,如CheckStyle、FindBug、Klocwork等。这些代码校验工具有一些是基于Java的源码进行校验,还有一些是通过扫描字节码来完成,在本节的实战中,我们将会使用注解处理器API来编写一款拥有自己编码风格的校验工具:NameCheckProcessor。

  当然,由于我们的实战都是为了学习和演示技术原理,而不是为了做出一款能媲美CheckStyle等工具的产品来,所以NameCheckProcessor的目标也仅定为对Java程序命名进行检查,根据《Java语言规范(第3版)》中第6.8节的要求,Java程序命名应当符合下列格式的书写规范。

  • 类(或接口):符合驼式命名法,首字母大写。
  • 方法:符合驼式命名法,首字母小写。
  • 字段:

    类或实例变量:符合驼式命名法,首字母小写。

    常量:要求全部由大写字母或下划线构成,并且第一个字符不能是下划线。

  上文提到的驼式命名法(Camel Case Name),正如它的名称所表示的那样,是指混合使用大小写字母来分割构成变量或函数的名字,犹如驼峰一般,这是当前Java语言中主流的命名规范,我们的实战目标就是为Javac编译器添加一个额外的功能,在编译程序时检查程序名是否符合上述对类(或接口)、方法、字段的命名要求[1]。

4.2 代码实现

  要通过注解处理器API实现一个编译器插件,首先需要了解这组API的一些基本知识。我们实现注解处理器的代码需要继承抽象类javax.annotation.processing.AbstractProcessor,这个抽象类中只有一个必须覆盖的abstract方法:“process()”,它是Javac编译器在执行注解处理器代码时要调用的过程,我们可以从这个方法的第一个参数“annotations”中获取到此注解处理器所要处理的注解集合,从第二个参数“roundEnv”中访问到当前这个Round中的语法树节点,每个语法树节点在这里表示为一个Element。在JDK 1.6新增的javax.lang.model包中定义了16类Element,包括了Java代码中最常用的元素,如:“包(PACKAGE)、枚举(ENUM)、类(CLASS)、注解(ANNOTATION_TYPE)、接口(INTERFACE)、枚举值(ENUM_CONSTANT)、字段(FIELD)、参数(PARAMETER)、本地变量(LOCAL_VARIABLE)、异常(EXCEPTION_PARAMETER)、方法(METHOD)、构造函数(CONSTRUCTOR)、静态语句块(STATIC_INIT,即static{}块)、实例语句块(INSTANCE_INIT,即{}块)、参数化类型(TYPE_PARAMETER,既泛型尖括号内的类型)和未定义的其他语法树节点(OTHER)”。除了process()方法的传入参数之外,还有一个很常用的实例变量“processingEnv”,它是AbstractProcessor中的一个protected变量,在注解处理器初始化的时候(init()方法执行的时候)创建,继承了AbstractProcessor的注解处理器代码可以直接访问到它。它代表了注解处理器框架提供的一个上下文环境,要创建新的代码、向编译器输出信息、获取其他工具类等都需要用到这个实例变量。

  注解处理器除了process()方法及其参数之外,还有两个可以配合使用的Annotations:@SupportedAnnotationTypes和@SupportedSourceVersion,前者代表了这个注解处理器对哪些注解感兴趣,可以使用星号“*”作为通配符代表对所有的注解都感兴趣,后者指出这个注解处理器可以处理哪些版本的Java代码。

  每一个注解处理器在运行的时候都是单例的,如果不需要改变或生成语法树的内容,process()方法就可以返回一个值为false的布尔值,通知编译器这个Round中的代码未发生变化,无须构造新的JavaCompiler实例,在这次实战的注解处理器中只对程序命名进行检查,不需要改变语法树的内容,因此process()方法的返回值都是false。关于注解处理器的API,作者就简单介绍这些,对这个领域有兴趣的读者可以阅读相关的帮助文档。下面来看看注解处理器NameCheckProcessor的具体代码,如代码清单10-11所示。

  代码清单10-11 注解处理器NameCheckProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//可以用"*"表示支持所有Annotations @SupportedAnnotationTypes("*") 
//只支持JDK 1.6的Java代码 @SupportedSourceVersion(SourceVersion.RELEASE_6)
public class NameCheckProcessor extends AbstractProcessor{
private NameChecker nameChecker;
/**
*初始化名称检查插件
*/
@Override
public void init(ProcessingEnvironment processingEnv){
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}

/**
* 对输入的语法树的各个节点进行名称检查
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
if(!roundEnv.processingOver()){
for(Element element : roundEnv.getRootElements())
nameChecker.checkNames(element);
}
return false;
}
}

  从上面代码可以看出,NameCheckProcessor能处理基于JDK 1.6的源码,它不限于特定的注解,对任何代码都“感兴趣”,而在process()方法中是把当前Round中的每一个RootElement传递到一个名为NameChecker的检查器中执行名称检查逻辑,NameChecker的代码如代码清单10-12所示。

  代码清单10-12 命名检查器NameChecker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/** 
* 程序名称规范的编译器插件:<br>
* 如果程序命名不合规范, 将会输出一个编译器的WARNING信息
*/
public class NameChecker{
private final Messager messager;
NameCheckScanner nameCheckScanner = new NameCheckScanner();

NameChecker(ProcessingEnvironment processsingEnv){
this.messager = processsingEnv.getMessager();
}

/**
*对Java程序命名进行检查, 根据《Java语言规范(第3版)》第6.8节的要求, Java程序命名应当符合下列格式:
*
* <ul>
* <li>类或接口:符合驼式命名法, 首字母大写。
* <li>方法:符合驼式命名法, 首字母小写。
* <li>字段:
* <ul>
* <li>类、实例变量:符合驼式命名法, 首字母小写。
* <li>常量:要求全部大写。
* </ul>
* </ul>
*/
public void checkNames(Element element){
nameCheckScanner.scan(element);
}

/**
* 名称检查器实现类, 继承了JDK 1.6中新提供的ElementScanner6<br>
* 将会以Visitor模式访问抽象语法树中的元素
*/
private class NameCheckScanner extends ElementScanner6<Void, Void>{
/**
* 此方法用于检查Java类
*/
@Override
public Void visitType(TypeElement e, Void p){
scan(e.getTypeParameters(), p);
checkCamelCase(e, true);
super.visitType(e, p);
return null;
}

/**
* 检查方法命名是否合法
*/
@Override
public Void visitExecutable(ExecutableElement e, Void p){
if(e.getKind() == METHOD){
Name name=e.getSimpleName();
if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
messager.printMessage(WARNING, "一个普通方法" + name + "不应当与类名重复, 避免与构造函数产生混淆", e);

checkCamelCase(e, false);
}
super.visitExecutable(e, p);
return null;
}

/**
* 检查变量命名是否合法
*/
@Override
public Void visitVariable(VariableElement e, Void p){
//如果这个Variable是枚举或常量, 则按大写命名检查, 否则按照驼式命名法规则检查
if(e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
checkAllCaps(e);
else
checkCamelCase(e,false);
return null;
}

/**
* 判断一个变量是否是常量
*/
private boolean heuristicallyConstant(VariableElement e){
if(e.getEnclosingElement().getKind() == INTERFACE)
return true;
else if(e.getKind() == FIELD&&e.getModifiers().containsAll(EnumSet.of(PUBLIC,STATIC,FINAL)))
return true;
else{
return false;
}
}

/**
* 检查传入的Element是否符合驼式命名法, 如果不符合, 则输出警告信息
*/
private void checkCamelCase(Element e, boolean initialCaps){
String name = e.getSimpleName().toString();
boolean previousUpper = false;
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if(Character.isUpperCase(firstCodePoint)){
previousUpper = true;
if(!initialCaps){
messager.printMessage(WARNING, "名称""+name+""应当以小写字母开头", e);
return;
}
}else if(Character.isLowerCase(firstCodePoint)){
if(initialCaps){
messager.printMessage(WARNING, "名称""+name+""应当以大写字母开头", e);
return;
}
}else
conventional = false;

if(conventional){
int cp = firstCodePoint;
for(int i = Character.charCount(cp);i<name.length();i += Character.charCount(cp)){
cp = name.codePointAt(i);
if(Character.isUpperCase(cp)){
if(previousUpper){
conventional = false;
break;
}
previousUpper = true;
}else
previousUpper = false;
}
}

if(!conventional)
messager.printMessage(WARNING, "名称""+name+""应当符合驼式命名法(Camel Case Names)", e);
}

/**
*大写命名检查, 要求第一个字母必须是大写的英文字母, 其余部分可以是下划线或大写字母
*/
private void checkAllCaps(Element e){
String name = e.getSimpleName().toString();
boolean conventional = true;
int firstCodePoint = name.codePointAt(0);
if(!Character.isUpperCase(firstCodePoint))
conventional = false;
else{
boolean previousUnderscore = false;
int cp = firstCodePoint;
for(int i = Character.charCount(cp);i<name.length();i+ = Character.charCount(cp)){
cp = name.codePointAt(i);
if(cp == (int) '_'){
if(previousUnderscore){
conventional = false;
break;
}
previousUnderscore = true;
}else{
previousUnderscore = false;
if(!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
conventional = false;
break;
}
}
}
}

if(!conventional)
messager.printMessage(WARNING, "常量" + name + "应当全部以大写字母或下划线命名, 并且以字母开头", e);
}
}
}

  NameChecker的代码看起来有点长,但实际上注释占了很大一部分,其实即使算上注释也不到190行。它通过一个继承于javax.lang.model.util.ElementScanner6的NameCheckScanner类,以Visitor模式来完成对语法树的遍历,分别执行visitType()、visitVariable()和visitExecutable()方法来访问类、字段和方法,这3个visit方法对各自的命名规则做相应的检查,checkCamelCase()与checkAllCaps()方法则用于实现驼式命名法和全大写命名规则的检查。

  整个注解处理器只需NameCheckProcessor和NameChecker两个类就可以全部完成,为了验证我们的实战成果,代码清单10-13中提供了一段命名规范的“反面教材”代码,其中的每一个类、方法及字段的命名都存在问题,但是使用普通的Javac编译这段代码时不会提示任何一个Warning信息。

  代码清单10-13 包含了多处不规范命名的代码样例

1
2
3
4
5
6
7
8
9
10
11
12
public class BADLY_NAMED_CODE{ 
enum colors{ red,blue,green; }
static final int_FORTY_TWO = 42;
public static int NOT_A_CONSTANT = _FORTY_TWO;

protected void BADLY_NAMED_CODE(){
return;
}
public void NOTcamelCASEmethodNAME(){
return;
}
}

4.3 运行与测试

  我们可以通过Javac命令的“-processor”参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号分隔。还可以使用-XprintRounds和-XprintProcessorInfo参数来查看注解处理器运作的详细信息,本次实战中的NameCheckProcessor的编译及执行过程如代码清单10-14所示。

  代码清单10-14 注解处理器的运行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
D:\src>javac org/fenixsoft/compile/NameChecker.java 
D:\src>javac org/fenixsoft/compile/NameCheckProcessor.java
D:\src>javac-processor org.fenixsoft.compile.NameCheckProcessor org/fenixsoft/compile/BADLY_NAMED_CODE.java

org\fenixsoft\compile\BADLY_NAMED_CODE.java:3:警告:名称"BADLY_NAMED_CODE"应当符合驼式命名法(Camel Case Names)

public class BADLY_NAMED_CODE{
^
org\fenixsoft\compile\BADLY_NAMED_CODE.java:5:警告:名称"colors"应当以大写字母开头
enum colors{
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:6:警告:常量"red"应当全部以大写字母或下划线命名, 并且以字母开头
red,blue,green;
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:6:警告:常量"blue"应当全部以大写字母或下划线命名, 并且以字母开头
red,blue,green;
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:6:警告:常量"green"应当全部以大写字母或下划线命名, 并且以字母开头
red,blue,green;
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:9:警告:常量"_FORTY_TWO"应当全部以大写字母或下划线命名, 并且以字母开头
static final int_FORTY_TWO=42;
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:11:警告:名称"NOT_A_CONSTANT"应当以小写字母开头
public static int NOT_A_CONSTANT=_FORTY_TWO;
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:13:警告:名称"Test"应当以小写字母开头
protected void Test(){
^ org\fenixsoft\compile\BADLY_NAMED_CODE.java:17:警告:名称"NOTcamelCASEmethodNAME"应当以小写字母开头
public void NOTcamelCASEmethodNAME(){

4.4 其他应用案例

  NameCheckProcessor的实战例子只演示了JSR-269嵌入式注解处理器API中的一部分功能,基于这组API支持的项目还有用于校验Hibernate标签使用正确性的Hibernate Validator Annotation Processor[1](本质上与NameCheckProcessor所做的事情差不多)、自动为字段生成getter和setter方法的Project Lombok[2](根据已有元素生成新的语法树元素)等,读者有兴趣的话可以参考它们官方站点的相关内容。


参考博客和文章书籍等:

《深入理解Java虚拟机》

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容